package com.mo.tools.db;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.*;


/**
 * description: JPADB
 * author: e-Xingfu.Yang
 * date: 2022/5/11 17:20
 */
@SuppressWarnings("all")
public class JPADB {


    private static final Logger log = LoggerFactory.getLogger(JPADB.class);
    protected static final Map<Type, JpaRepositoryFactoryBean> repositoryMap = new LinkedHashMap<>(16);


    public static <T> T save(T entity) {
        return (T) getRepository(entity.getClass()).save(entity);
    }

    public static <T> List<T> saveAll(Iterable<T> entities) {
        if (ObjectUtils.isEmpty(entities)) {
            return null;
        } else {
            return getRepository(entities.iterator().next().getClass()).saveAll(entities);
        }
    }

    public static <T> void delete(T entity) {
        getRepository(entity.getClass()).delete(entity);
    }

    public static <T> void deleteById(Class<T> tableClass, Serializable id) {
        getRepository(tableClass).deleteById(getActualId(tableClass, id));
    }

    public static <T> void deleteByIds(@NotNull Class<T> tableClass, Iterable<Serializable> ids) {
        if (!ObjectUtils.isEmpty(ids)) {
            List<T> entities = new ArrayList<>(10);
            Field[] declaredFields = tableClass.getDeclaredFields();
            Field field = null;
            for (Field declaredField : declaredFields) {
                Id javaIdAnnotation = declaredField.getAnnotation(Id.class);
                if (javaIdAnnotation != null) {
                    field = declaredField;
                    break;
                }
            }
            if (field != null) {
                for (Serializable id : ids) {
                    try {
                        T entity = tableClass.newInstance();
                        field.setAccessible(true);
                        field.set(entity, getActualId(tableClass, id));
                        entities.add(entity);
                    } catch (Exception e) {
                        log.error("execute deleteByIds method failed", e);
                    }
                }
                field.setAccessible(false);
                getRepository(tableClass).deleteInBatch(entities);
            }
        }
    }

    public static <T> void deleteInBatch(Iterable<T> entities) {
        if (!ObjectUtils.isEmpty(entities)) {
            getRepository(entities.iterator().next().getClass()).deleteInBatch(entities);
        }
    }

    public static <T> void deleteAll(Class<T> tableClass) {
        getRepository(tableClass).deleteAll();
    }


    public static <T> T findById(Class<T> tableClass, Serializable id) {
        return (T) getRepository(tableClass).findById(getActualId(tableClass, id)).orElse(null);
    }

    public static <T> List<T> findByIds(Class<T> tableClass, Iterable<Serializable> ids) {
        return getRepository(tableClass).findAllById(getActualIds(tableClass, ids));
    }

    public static <T> T findOne(Example<T> example) {
        return (T) getRepository(example.getProbeType()).findOne(example).orElse(null);
    }

    public static <T> List<T> findAll(Class<T> tableClass) {
        return getRepository(tableClass).findAll();
    }

    public static <T> List<T> findAll(Class<T> tableClass, Sort sort) {
        return getRepository(tableClass).findAll(sort);
    }

    public static <T> List<T> findAll(Example<T> example) {
        return getRepository(example.getProbeType()).findAll(example);
    }

    public static <T> List<T> findAll(Example<T> example, Sort sort) {
        return getRepository(example.getProbeType()).findAll(example, sort);
    }

    public static <T> Page<T> findAll(Class<T> tableClass, Pageable pageable) {
        return getRepository(tableClass).findAll(pageable);
    }

    public static <T> Page<T> findAll(Example<T> example, Pageable pageable) {
        return getRepository(example.getProbeType()).findAll(example, pageable);
    }

    public static <T> long count(Example<T> example) {
        return getRepository(example.getProbeType()).count(example);
    }

    public static <T> long count(Class<T> tableClass) {
        return getRepository(tableClass).count();
    }

    public static <T> boolean exists(Example<T> example) {
        return getRepository(example.getProbeType()).exists(example);
    }

    public static <T> boolean existsById(Class<T> tableClass, Serializable id) {
        return getRepository(tableClass).existsById(getActualId(tableClass, id));
    }

    private static <T> Object getActualId(Class<T> tableClass, Serializable id) {
        if (ObjectUtils.isEmpty(id)) {
            throw new RuntimeException("id is empty");
        }
        return getActualIds(tableClass, Collections.singleton(id)).get(0);

    }

    private static <T> List<Object> getActualIds(Class<T> tableClass, Iterable<Serializable> ids) {
        if (tableClass == null) {
            throw new RuntimeException("class type is empty");
        }
        JpaRepositoryFactoryBean repositoryFactoryBean = repositoryMap.get(tableClass);
        if (ObjectUtils.isEmpty(repositoryFactoryBean)) {
            throw new RuntimeException("not found entity mapping repository interface");
        }
        Class idType = repositoryFactoryBean.getEntityInformation().getIdType();
        if (idType == null) {
            throw new RuntimeException("primary key class type is empty");
        }

        if (ObjectUtils.isEmpty(ids)) {
            throw new RuntimeException("ids is empty");
        }
        List<Object> idList = new ArrayList<>();
        for (Serializable id : ids) {
            String idStr = String.valueOf(id);
            if (Long.class.equals(idType)) {
                idList.add(Long.parseLong(idStr));
            } else if (Integer.class.equals(idType)) {
                idList.add(Integer.parseInt(idStr));
            } else if (String.class.equals(idType)) {
                idList.add(idStr);
            } else {
                throw new RuntimeException("id type error");
            }
        }
        return idList;

    }


    public static JpaRepository getRepository(Class<?> tableClass) {
        if (ObjectUtils.isEmpty(tableClass)) {
            throw new RuntimeException("entity class type is not empty");
        }
        JpaRepositoryFactoryBean repositoryFactoryBean = repositoryMap.get(tableClass);
        if (ObjectUtils.isEmpty(repositoryFactoryBean)) {
            throw new RuntimeException("not found entity mapping repository interface");
        }
        return (JpaRepository) repositoryFactoryBean.getObject();
    }

    @Component
    private static class DbBeanPostProcessor implements BeanPostProcessor {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

            if (bean instanceof JpaRepositoryFactoryBean) {
                JpaRepositoryFactoryBean repositoryFactoryBean = (JpaRepositoryFactoryBean) bean;
                Class repositoryType = repositoryFactoryBean.getObjectType();
                EntityInformation entityInformation = repositoryFactoryBean.getEntityInformation();
                Class entityType = entityInformation.getJavaType();
                repositoryMap.put(entityType, repositoryFactoryBean);
            }

            return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
        }
    }


}
